Skip to content

feat: Windows support#2119

Merged
dunglas merged 72 commits intomainfrom
windows
Feb 26, 2026
Merged

feat: Windows support#2119
dunglas merged 72 commits intomainfrom
windows

Conversation

@dunglas
Copy link
Member

@dunglas dunglas commented Jan 9, 2026

Closes #83 #880 #1286.

Working patch for Windows support.

Supports linking to the official PHP release (TS version).
Includes some work from #1286 (thanks @TenHian!!)

This patch allows using Visual Studio to compile the cgo code. To do so, it must be compiled with Go 1.26 (RC) with the following setup:

winget install -e --id Microsoft.VisualStudio.2022.Community --override "--passive --wait --add Microsoft.VisualStudio.Workload.NativeDesktop --add Microsoft.VisualStudio.Component.VC.Llvm.Clang --includeRecommended"
winget install -e --id GoLang.Go

$env:PATH += ';C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\Llvm\bin'

cd c:\
gh repo clone microsoft/vcpkg
.\vcpkg\bootstrap-vcpkg.bat
.\vcpkg\vcpkg install pthreads brotli

# build watcher
Invoke-WebRequest -Uri "https://github.com/e-dant/watcher/releases/download/0.14.3/x86_64-pc-windows-msvc.tar" -OutFile "$env:TEMP\watcher.tar"
tar -xf "$env:TEMP\watcher.tar" -C C:\
Rename-Item -Path "C:\x86_64-pc-windows-msvc" -NewName "watcher-x86_64-pc-windows-msvc"
Remove-Item "$env:TEMP\watcher.tar"

# download php
Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php.zip"
Expand-Archive -Path "$env:TEMP\php.zip" -DestinationPath "C:\"
Remove-Item "$env:TEMP\php.zip"

# download php development package
Invoke-WebRequest -Uri "https://downloads.php.net/~windows/releases/archives/php-devel-pack-8.5.1-Win32-vs17-x64.zip" -OutFile "$env:TEMP\php-devel.zip"
Expand-Archive -Path "$env:TEMP\php-devel.zip" -DestinationPath "C:\"
Remove-Item "$env:TEMP\php-devel.zip"

$env:GOTOOLCHAIN = 'go1.26rc1'
$env:CC = 'clang'
$env:CXX = 'clang++'
$env:CGO_CFLAGS = "-I$env:C:\vcpkg\installed\x64-windows\include -IC:\watcher-x86_64-pc-windows-msvc -IC:\php-8.5.1-devel-vs17-x64\include -IC:\php-8.5.1-devel-vs17-x64\include\main -IC:\php-8.5.1-devel-vs17-x64\include\TSRM -IC:\php-8.5.1-devel-vs17-x64\include\Zend -IC:\php-8.5.1-devel-vs17-x64\include\ext"
$env:CGO_LDFLAGS = '-LC:\vcpkg\installed\x64-windows\lib -lbrotlienc -LC:\watcher-x86_64-pc-windows-msvc -llibwatcher-c -LC:\php-8.5.1-Win32-vs17-x64 -LC:\php-8.5.1-devel-vs17-x64\lib -lphp8ts -lphp8embed'

# clone frankenphp and build
git clone -b windows https://github.com/php/frankenphp.git
cd frankenphp\caddy\frankenphp
go build -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx

# Tests

$env:PATH += ";$env:VCPKG_ROOT\installed\x64-windows\bin;C:\watcher-x86_64-pc-windows-msvc";C:\php-8.5.1-Win32-vs17-x64"
"opcache.enable=0`r`nopcache.enable_cli=0" | Out-File -Encoding ascii php.ini
$env:PHPRC = Get-Location
go test -ldflags '-extldflags="-fuse-ld=lld"' -tags nowatcher,nobadger,nomysql,nopgx .

TODO:

  • Fix remaining skipped tests (scaling and watcher)
  • Test if the watcher mode works as expected
  • Automate the build with GitHub Actions

@henderkes
Copy link
Contributor

Very interesting, I had no idea that lld could link MinGW and MSVC objects together. This links against php8embed.lib?

@dunglas
Copy link
Member Author

dunglas commented Jan 9, 2026

Mingw isn't used at all with this patch. Everything uses Visual Studio.
For cgo, it uses the clang version provided by VS, which has a VS backend (needs Go 1.26, I stumbled upon this undocumented Google patch very recently).

php8embed.dll is not even needed, only php8ts.dll is needed, because I disabled the php-cli subcommand.

Locally, I even manage to run php-cli, but this requires manually compiling PHP to enable the embed SAPI, so I disabled it for now because both php8ts.dll and php.exe are shipped with the official PHP package.

I also have a patch for Static PHP CLI, but it's not working because the PHP source code and Makefile on Windows doesn't support building a static version of php8ts.dll and php8embed.dll. Patching the PHP source code will be necessary.

@henderkes
Copy link
Contributor

For cgo, it uses the clang version provided by VS, which has a VS backend (needs Go 1.26, I stumbled upon this undocumented Google patch very recently).

That's the part I was missing, thank you!

@henderkes
Copy link
Contributor

henderkes commented Jan 10, 2026

Gave it a shot and updated your initial post for instructions, however, once it attempts to serve a php file with .\frankenphp.exe php-server --root=./ the app stops.

@dunglas
Copy link
Member Author

dunglas commented Jan 10, 2026

Have you copied all the necessary DDLs in the same directory?

@henderkes
Copy link
Contributor

Shouldn't be necessary with

$env:PATH += ";$env:VCPKG_ROOT\installed\x64-windows\bin"
$env:PATH += ";C:\watcher-x86_64-pc-windows-msvc"
$env:PATH += ";C:\php-8.5.1-Win32-vs17-x64"

Is anything else required?

@dunglas
Copy link
Member Author

dunglas commented Jan 10, 2026

You must also add $env:PATH += ";C:\php-8.5.1-Win32-vs17-x64\lib".

@henderkes
Copy link
Contributor

Same issue, I don't think it could be related to libraries anyway, as it would fail to run in the first place then. Shared libraries are (by default) loaded at initialisation time and we're not passing delayload arguments to the compilation.

Log:

❯❯ frankenphp git:(windows) 21:46 .\frankenphp.exe php-server --root=./
2026/01/10 20:46:22.912 WARN    admin   admin endpoint disabled
2026/01/10 20:46:22.912 INFO    tls.cache.maintenance   started background certificate maintenance      {"cache": "0x2e61d3850500"}
2026/01/10 20:46:22.912 WARN    http.auto_https server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server {"server_name": "php", "http_port": 80}
2026/01/10 20:46:22.949 INFO    frankenphp      FrankenPHP started 🐘   {"php_version": "8.5.1", "num_threads": 64, "max_threads": 64}
2026/01/10 20:46:22.950 WARN    http    HTTP/2 skipped because it requires TLS  {"network": "tcp", "addr": ":80"}
2026/01/10 20:46:22.950 WARN    http    HTTP/3 skipped because it requires TLS  {"network": "tcp", "addr": ":80"}
2026/01/10 20:46:22.950 INFO    http.log        server running  {"name": "php", "protocols": ["h1", "h2", "h3"]}
2026/01/10 20:46:22.951 INFO    Caddy serving PHP app on :80
2026/01/10 20:46:22.953 INFO    tls     storage cleaning happened too recently; skipping for now        {"storage": "FileStorage:C:\\Users\\m\\AppData\\Roaming\\Caddy", "instance": "489ade52-21ab-40c6-b18a-2932fb8eab4d", "try_again": "2026/01/11 20:46:22.953", "try_again_in": 86400}
2026/01/10 20:46:22.953 INFO    tls     finished cleaning storage units
❯❯ frankenphp git:(windows)  21:46

Text files like an index.html page are served just fine. Only when php is attempted to be executed does the program simply stop.

@dunglas
Copy link
Member Author

dunglas commented Jan 10, 2026

I didn't try the php-server command yet, only run with a custom Caddyfile.
Are the tests green? (They are on my local installation)

@dunglas
Copy link
Member Author

dunglas commented Jan 10, 2026

Could you also show me the content of your PHP script?

@henderkes
Copy link
Contributor

Content of the php script:

<?php
echo phpinfo();

I haven't ran the test suite yet, but frankenphp run shows the same behaviour.

@dunglas dunglas marked this pull request as ready for review January 12, 2026 13:43
@dunglas
Copy link
Member Author

dunglas commented Jan 12, 2026

Here is a build I created locally: https://drive.google.com/file/d/1B09de1rERpRUN-bnAje0K2vHcwhVoDJa/view?usp=sharing
It's the official PHP binary distribution with FrankenPHP itself and the extra needed DLLs added.

On my computer, it runs Symfony without issue @henderkes.

@henderkes
Copy link
Contributor

henderkes commented Jan 12, 2026

Thanks, I'll try this one and report back.

Edit: it's working, but it was linked against 8.5.3-dev. I'll try to link against 8.5.1 again and see if I can get anywhere after the new commits.

@henderkes
Copy link
Contributor

image

@henderkes
Copy link
Contributor

@dunglas I've prepared everything except for the failing tests.

We cannot switch to watcher-c from vcpkg because they included the header-only c++ version, so it's not usable for us.

@dunglas
Copy link
Member Author

dunglas commented Feb 25, 2026

Thank you vert much!

What tests are failing? The CI looks green.

@henderkes
Copy link
Contributor

henderkes commented Feb 25, 2026

You set the windows workflow to upload first and run tests later (ignoring errors), so the CI looks green but the go test run is failing: https://github.com/php/frankenphp/actions/runs/22378079619/job/64772633591

?   	github.com/dunglas/frankenphp/internal/cpu	[no test files]
Warning: Go method signature mismatch for 'User::getName': return type mismatch: PHP "string" requires Go return type "unsafe.Pointer" but found "string"
Warning: Go method signature mismatch for 'TestClass::arrayMethod': parameter 1 type mismatch: PHP "array" requires Go type "*C.zend_array" but found "any"
Warning: Method "TestClass::objectMethod" uses unsupported types: parameter 1 "obj" has unsupported type "object", supported typed: string, int, float, bool, array and mixed, can be nullable
Warning: Go method signature mismatch for 'TestClass::mixedMethod': parameter 1 type mismatch: PHP "mixed" requires Go type "*C.zval" but found "any"
Warning: Go method signature mismatch for 'TestClass::arrayReturn': return type mismatch: PHP "array" requires Go return type "unsafe.Pointer" but found "any"
Warning: Method "TestClass::objectReturn" uses unsupported types: return type "object" is not supported, supported typed: string, int, float, bool, array and mixed, can be nullable
Warning: Go method signature mismatch for 'TestClass::countMismatch': parameter count mismatch: PHP function has 2 parameters (expecting 2 Go parameters) but Go function has 1
Warning: Go method signature mismatch for 'TestClass::typeMismatch': parameter 2 type mismatch: PHP "int" requires Go type "int64" but found "string"
Warning: Go method signature mismatch for 'TestClass::returnMismatch': return type mismatch: PHP "int" requires Go return type "int64" but found "string"
Warning: Go function signature mismatch for "arrayFunc": parameter 1 type mismatch: PHP "array" requires Go type "*C.zend_array" but found "any"
Warning: Function 'objectFunc' uses unsupported types: parameter 1 "obj" has unsupported type "object", supported typed: string, int, float, bool, array and mixed, can be nullable
Warning: Go function signature mismatch for "mixedFunc": parameter 1 type mismatch: PHP "mixed" requires Go type "*C.zval" but found "any"
Warning: Go function signature mismatch for "arrayReturnFunc": return type mismatch: PHP "array" requires Go return type "unsafe.Pointer" but found "any"
Warning: Function 'objectReturnFunc' uses unsupported types: return type "object" is not supported, supported typed: string, int, float, bool, array and mixed, can be nullable
Warning: Go function signature mismatch for "countMismatch": parameter count mismatch: PHP function has 2 parameters (expecting 2 Go parameters) but Go function has 1
Warning: Go function signature mismatch for "typeMismatch": parameter 2 type mismatch: PHP "int" requires Go type "int64" but found "string"
Warning: Go function signature mismatch for "returnMismatch": return type mismatch: PHP "int" requires Go return type "int64" but found "string"
--- FAIL: TestStubGenerator_FileStructure (0.00s)
    stub_test.go:547: 
        	Error Trace:	D:/a/frankenphp/frankenphp/frankenphp/internal/extgen/stub_test.go:547
        	Error:      	"1" is not greater than or equal to "3"
...

It's only the extension generator tests though, the rest is OK.

@henderkes
Copy link
Contributor

This should be good to merge then? 🥳

@dunglas dunglas merged commit 25ed020 into main Feb 26, 2026
96 of 97 checks passed
@dunglas dunglas deleted the windows branch February 26, 2026 11:38
@dunglas
Copy link
Member Author

dunglas commented Feb 26, 2026

This is a huge milestone!! Thank you to everyone involved, especially you, @TenHian, and @henderkes.

@MisterDuval
Copy link

Any chance to get a build before official one? I'm so excited about that!

@henderkes
Copy link
Contributor

https://github.com/php/frankenphp/actions/runs/22440426838/job/64981105036

Our CI uploads binaries. Though it's obviously not meant for production use as it doesn't have version variables & co set.

@Serveronet
Copy link

Serveronet commented Feb 27, 2026

I played around with CI uploads binaries. All works with Laravel. Great achievement!
But my question is, is this the target performance on Windows? Because it's somewhat slower.

@MisterDuval
Copy link

I played with it too, it is as "slow" as Caddy + php-cgi.exe on my side.

@henderkes
Copy link
Contributor

I would be surprised if it was much faster in latency, though throughput should be better. If it isn't, I'll take a look.

The inherent issue with php in windows is that php runs very poorly on windows in general. Many optimisations are done specifically for unix or even gnu-linux, for typical linux compilers gcc (and only now clang, without preserve_none support on windows) and then, even worse, the windows file system and intrusive file scanning make it extremely slow to open files, which php does a lot. A composer install takes 10x longer on the first run on windows compared to in WSL.

That's a difference no webserver can "fix". The only "fix" for it is using a system suitable for php, which windows simply isn't.

@henderkes
Copy link
Contributor

So I've played around with it with the symfony/demo repository. First, response times of ~800ms, after disabling Antivirus it's 250-400ms. With worker mode it's 120ms.

I'll set up mirrored wsl mode and play with wrk a bit.

@henderkes
Copy link
Contributor

Results look about as I expected. FrankenPHP is much faster than untuned Apache. Of course it's still awfully slow, it's Windows after all, but that's not something we can fix. This is with Antivirus disabled, with it enabled multiply the results by 2. It's bad.

XAMPP (updated to php 8.5, default configuration):

❯❯ m  10:10 wrk -t4 -c20 -d15s http://localhost/
Running 15s test @ http://localhost/
  4 threads and 20 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.91s    58.67ms   2.00s    64.10%
    Req/Sec     4.88      6.29    30.00     91.67%
  140 requests in 15.05s, 7.17MB read
  Socket errors: connect 0, read 0, write 0, timeout 62
Requests/sec:      9.30
Transfer/sec:    487.67KB
❯❯ m  10:10 wrk -t4 -c10 -d15s http://localhost/
Running 15s test @ http://localhost/
  4 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   777.57ms   64.55ms 905.77ms   67.11%
    Req/Sec     3.62      3.32    10.00     81.30%
  152 requests in 15.05s, 7.78MB read
Requests/sec:     10.10
Transfer/sec:    529.51KB

FrankenPHP (php-server):

❯❯ m  10:10 wrk -t4 -c20 -d15s http://localhost/
Running 15s test @ http://localhost/
  4 threads and 20 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   285.64ms   41.78ms 643.61ms   93.55%
    Req/Sec    18.29      8.78    40.00     77.21%
  1047 requests in 15.02s, 53.67MB read
Requests/sec:     69.69
Transfer/sec:      3.57MB
❯❯ m  10:11 wrk -t4 -c10 -d15s http://localhost/
Running 15s test @ http://localhost/
  4 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   150.11ms    8.67ms 199.89ms   79.90%
    Req/Sec    14.06      4.95    20.00     57.73%
  796 requests in 15.02s, 40.93MB read
Requests/sec:     52.99
Transfer/sec:      2.72MB

FrankenPHP (php-server) worker mode:

❯❯ m  10:12 wrk -t4 -c20 -d15s http://localhost/
Running 15s test @ http://localhost/
  4 threads and 20 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   161.75ms   44.25ms 618.68ms   96.02%
    Req/Sec    31.81     11.32    50.00     62.52%
  1872 requests in 15.02s, 95.77MB read
Requests/sec:    124.63
Transfer/sec:      6.38MB
❯❯ m  10:13 wrk -t4 -c10 -d15s http://localhost/
Running 15s test @ http://localhost/
  4 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    81.81ms    5.95ms 138.47ms   73.45%
    Req/Sec    24.41      6.74    40.00     89.67%
  1465 requests in 15.02s, 74.95MB read
Requests/sec:     97.53
Transfer/sec:      4.99MB

And for fun, FrankenPHP on Linux:

❯❯ m  10:14 wrk -t4 -c20 -d15s http://localhost/
Running 15s test @ http://localhost/
  4 threads and 20 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    35.29ms   14.55ms 124.89ms   69.18%
    Req/Sec   143.15     20.02   212.00     72.83%
  8580 requests in 15.06s, 420.52MB read
Requests/sec:    569.82
Transfer/sec:     27.93MB
❯❯ m  10:18 wrk -t4 -c10 -d15s http://localhost/
Running 15s test @ http://localhost/
  4 threads and 10 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    15.90ms    2.22ms  37.58ms   76.65%
    Req/Sec   126.18      9.49   160.00     76.67%
  7547 requests in 15.02s, 369.89MB read
Requests/sec:    502.62
Transfer/sec:     24.63MB

Tl;dr: just don't use Windows. Don't do it to yourself.

@dunglas
Copy link
Member Author

dunglas commented Mar 5, 2026

Thanks for the benchs! Is yiur linux benchmark on WSL?

@henderkes
Copy link
Contributor

Yeah on WSL, same machine.

@tobfel
Copy link

tobfel commented Mar 5, 2026

I also want to run a benchmark against frankenphp windows-binary vs. ubuntu-24.04 - wsl.
sadly i can only use wsl-v1 because of the kvm - host where the windows-server 2022 is running on.
I installed frankenphp via curl https://frankenphp.dev/install.sh | sh
but when i want to spin up a php-server i get:

frankenphp php-server
2026/03/05 07:55:37.665 WARN    admin   admin endpoint disabled
2026/03/05 07:55:37.666 WARN    http.auto_https server is listening only on the HTTP port, so no automatic HTTPS will be applied to this server {"server_name": "php", "http_port": 80}
2026/03/05 07:55:37.666 INFO    tls.cache.maintenance   started background certificate maintenance      {"cache": "0x2bb1ca662680"}
2026/03/05 07:55:37.695 ERROR   frankenphp      PHP Fatal error:  Could not create timer: Invalid argument (22) in Unknown on line 0
Stack trace:
#0 {main}       {"syslog_level": "err"}
github.com/dunglas/frankenphp.go_log
        /__w/packages/packages/source/frankenphp/frankenphp.go:710
_cgoexp_473f47800b4e_go_log
        /__w/packages/packages/source/frankenphp/frankenphp.go:658
runtime.cgocallbackg1
        /__w/packages/packages/pkgroot/x86_64-linux/go-xcaddy/src/runtime/cgocall.go:466
runtime.cgocallbackg
        /__w/packages/packages/pkgroot/x86_64-linux/go-xcaddy/src/runtime/cgocall.go:362
runtime.cgocallback
        /__w/packages/packages/pkgroot/x86_64-linux/go-xcaddy/src/runtime/asm_amd64.s:1160

Any hints on this:

  • is wsl-v1 not supported?
  • some dependencies missing?
  • something to tweak for the start via Caddyfile settings/environment vars?
  • need to compile my own with different options?

Thank you.

@henderkes
Copy link
Contributor

is wsl-v1 not supported?

No, definitely not. The first iteration of WSL was beyond broken. Use a real VM instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows compatibility

6 participants